﻿// Microsoft Public License (Ms-PL)
// Copyright (c) 2010 Nick Darnell
//
// http://www.nickdarnell.com
// http://wpfshadergenerator.codeplex.com
//
// A fair amount of the shader register parsing code was originally taken
// from the Shazzam project so that compatability could be maintained
// with the project so that users could continue to use Shazzam for
// testing their shaders.  However it has been modified and added onto
// but in a way that should not break backwards compatability.
//
// Microsoft Public License (Ms-PL)
// http://shazzam.codeplex.com/

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Windows.Media;
using System.Windows;
using System.Windows.Media.Media3D;
using System.Windows.Media.Effects;

namespace WPFShaderEffectGenerator
{
    public static class ShaderFragmentReader
    {
        // Regular expression that matches a comment from double-slash to end-of-line (but not a triple-slash comment):
        private static readonly Regex CommentRegex = new Regex(@"(?<!/)//$|(?<!/)//[^/].*?$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);

        private const string AnythingPattern = @"[^;]*";
        private const string RequiredWhitespacePattern = @"\s+";
        private const string OptionalWhitespacePattern = @"\s*";

        // Patterns that match special triple-slash comments in the header:
        private const string DescriptionPattern = @"<description>(?<description>" + AnythingPattern + ")</description>";
        private const string ProfilePattern = @"<profile>(?<profile>" + AnythingPattern + ")</profile>";
        private const string TargetPattern = @"<target>(?<target>" + AnythingPattern + ")</target>";
        private const string DdxUvDdyUvRegisterIndexPattern = @"<DdxUvDdyUvRegisterIndex>(?<DdxUvDdyUvRegisterIndex>\d+)</DdxUvDdyUvRegisterIndex>";
        private const string HeaderCommentPattern = @"^///\s*(" + DescriptionPattern + @"|" + TargetPattern + @"|" + ProfilePattern + @"|" + DdxUvDdyUvRegisterIndexPattern + @")\s*?$\s*";
        private static readonly Regex HeaderCommentsRegex = new Regex(@"(" + HeaderCommentPattern + @")+",
            RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);

        // Patterns that match special triple-slash comments before each register declaration:
        private const string SummaryPattern = @"<summary>(?<summary>" + AnythingPattern + ")</summary>";
        private const string TypePattern = @"<type>(?<type>" + AnythingPattern + ")</type>";
        private const string DefaultValuePattern = @"<defaultValue>(?<defaultValue>" + AnythingPattern + ")</defaultValue>";
        private const string SamplingModePattern = @"<samplingMode>(?<samplingMode>" + AnythingPattern + ")</samplingMode>";
        private const string SpecialCommentPattern = @"^///\s*(" + SummaryPattern + @"|" + TypePattern + @"|" +
            SamplingModePattern + @"|" + DefaultValuePattern + @")\s*?$\s*";
        private const string SpecialCommentsPattern = @"(" + SpecialCommentPattern + @")*";

        private static readonly Regex RegisterCommentsRegex = new Regex(SpecialCommentsPattern, RegexOptions.IgnoreCase |
            RegexOptions.Compiled | RegexOptions.Multiline);

        // Comment block
        private const string CommentsBlockPattern = @"(?<comment>(^///([^\n;]*)\n)+)";

        // Patterns used in a constant register declaration in HLSL:
        private const string RegisterTypePattern = @"(?<registerType>(sampler2D|(half|float|int|uint|bool)[1234]?|float[1234]x[1234]))";
        private const string RegisterNamePattern = @"(?<registerName>\w+)";
        private const string RegisterConstantNumberPattern = @"[CcSsIiBb](?<registerNumber>\d+)";
        private const string InitializerValuePattern = @"(?<initializerValue>[^;]+)";
        private const string OptionalInitializerPattern = @"(?<initializer>=" + OptionalWhitespacePattern + InitializerValuePattern + OptionalWhitespacePattern + @")?";

        // Regular expression that matches an entire constant register declaration, including the preceding special comments:
        private static readonly Regex RegisterConstantDeclarationRegex = new Regex(CommentsBlockPattern +
            RegisterTypePattern + RequiredWhitespacePattern + RegisterNamePattern + OptionalWhitespacePattern +
            @":" + OptionalWhitespacePattern + @"register" + OptionalWhitespacePattern +
            @"\(" + OptionalWhitespacePattern + RegisterConstantNumberPattern + OptionalWhitespacePattern + @"\)" + OptionalWhitespacePattern +
            OptionalInitializerPattern + @";", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);

        public static LogErrorDelegate LogError { get; set; }
        public static LogWarningDelegate LogWarning { get; set; }

        public static ShaderFragment Read(string shaderFileName, string shaderText)
        {
            // Remove all double-slash comments (but not triple-slash comments).
            // This helps us avoid parsing register declarations that are commented out.
            shaderText = CommentRegex.Replace(shaderText, String.Empty);

            // Find all header comments.
            Match headerMatch = HeaderCommentsRegex.Match(shaderText);

            // Determine the class name, namespace, description, and target platform.
            string defaultClassName = Path.GetFileNameWithoutExtension(shaderFileName);
            defaultClassName = Char.ToUpperInvariant(defaultClassName[0]) + defaultClassName.Substring(1) + "Effect";
            string className = GetValidId(headerMatch.Groups["class"].Value, defaultClassName, false);
            string namespaceName = GetValidId(headerMatch.Groups["namespace"].Value, "MyShaders", true);
            string description = headerMatch.Groups["description"].Success ? headerMatch.Groups["description"].Value : null;
            string profile = headerMatch.Groups["profile"].Success ? headerMatch.Groups["profile"].Value : null;
            int DdxUvDdyUvRegisterIndex = headerMatch.Groups["DdxUvDdyUvRegisterIndex"].Success ? Int32.Parse(headerMatch.Groups["DdxUvDdyUvRegisterIndex"].Value) : -1;
            string targetProjectTypeName = headerMatch.Groups["target"].Success ? headerMatch.Groups["target"].Value : "WPF";
            TargetProjectType targetProjectType = targetProjectTypeName == "Silverlight" ? TargetProjectType.Silverlight : TargetProjectType.WPF;

            MatchCollection registerMatches = RegisterConstantDeclarationRegex.Matches(shaderText);

            List<ShaderFragmentRegister> registers = new List<ShaderFragmentRegister>();
            foreach (Match regMatch in registerMatches)
            {
                string registerTypeInHLSL = regMatch.Groups["registerType"].Value;
                
                Type registerType =
                    GetRegisterType(targetProjectType, registerTypeInHLSL);

                if (registerType != null)
                {
                    string comment = regMatch.Groups["comment"] == null ? null : regMatch.Groups["comment"].Value;
                    Match commentMatch = RegisterCommentsRegex.Match(comment);

                    // See if the user prefers to specify a different type in a comment.
                    if (commentMatch.Groups["type"].Success)
                    {
                        OverrideTypeIfAllowed(targetProjectType, commentMatch.Groups["type"].Value, ref registerType);
                    }

                    // Capitalize the first letter of the variable name.  Leave the rest alone.
                    string registerName = regMatch.Groups["registerName"].Value;
                    registerName = Char.ToUpperInvariant(registerName[0]) + registerName.Substring(1);

                    // Get the register number and the optional summary comment.
                    int registerNumber = Int32.Parse(regMatch.Groups["registerNumber"].Value);
                    string summary = commentMatch.Groups["summary"].Value;

                    // Get the standard min, max, and default value for the register type.
                    object defaultValue;
                    GetStandardValues(registerType, out defaultValue);

                    // Allow the user to override the defaults with values from their comments.
                    ConvertValue(commentMatch.Groups["defaultValue"].Value, registerType, ref defaultValue);

                    // And if the user specified an initializer for the register value in HLSL,
                    // that value overrides any other default value.
                    if (regMatch.Groups["initializer"].Success)
                    {
                        ParseInitializerValue(regMatch.Groups["initializerValue"].Value, registerType, ref defaultValue);
                    }

                    SamplingMode samplingMode = SamplingMode.Auto;
                    if (commentMatch.Groups["samplingMode"].Success)
                    {
                        try
                        {
                            samplingMode = (SamplingMode)Enum.Parse(typeof(SamplingMode),
                                commentMatch.Groups["samplingMode"].Value);
                        }
                        catch(Exception)
                        {
                            // Log error to UI
                        }
                    }

                    // Create a structure to hold the register information.
                    ShaderFragmentRegister register =
                        new ShaderFragmentRegister(registerName, registerType, registerNumber,
                            summary, defaultValue, samplingMode);

                    registers.Add(register);
                }
            }

            ShaderFragment fragment = new ShaderFragment
            {
                ShaderFileName = shaderFileName,
                Profile = profile,
                DdxUvDdyUvRegisterIndex = DdxUvDdyUvRegisterIndex,
                Description = description,
                TargetProjectType = targetProjectType,
                Registers = registers
            };

            return fragment;
        }

        private static Type GetRegisterType(TargetProjectType targetProjectType, string registerTypeInHLSL)
        {
            registerTypeInHLSL = registerTypeInHLSL.ToLower();
            switch (registerTypeInHLSL)
            {
                case "sampler2d":
                    return typeof(Brush);
                case "float":
                case "float1":
                    return typeof(double);
                case "float2":
                    return typeof(Point);
                case "float3":
                    // Silverlight doesn't have any types that correspond to float3 registers.
                    return targetProjectType == TargetProjectType.WPF ? typeof(Point3D) : null;
                case "float4":
                    return typeof(Color);
                case "int":
                case "int1":
                    return targetProjectType == TargetProjectType.WPF ? typeof(int) : null;
                case "uint":
                case "uint1":
                    return targetProjectType == TargetProjectType.WPF ? typeof(uint) : null;
                case "bool":
                case "bool1":
                    return targetProjectType == TargetProjectType.WPF ? typeof(bool) : null;
            }

            LogWarning(1, "Unable to map HLSL register type \"" + registerTypeInHLSL + "\" to a .Net type.", 0, 0);

            return null;
        }

        private static void OverrideTypeIfAllowed(TargetProjectType targetProjectType, string suggestedType, ref Type registerType)
        {
            suggestedType = suggestedType.ToLower();
            switch (suggestedType)
            {
                case "float":
                case "single":
                    if (registerType == typeof(double))
                        registerType = typeof(float);
                    break;
                case "size":
                    if (registerType == typeof(Point))
                        registerType = typeof(Size);
                    break;
                case "vector":
                    if (registerType == typeof(Point))
                        registerType = typeof(Vector);
                    break;
                case "vector3d":
                    // Silverlight doesn't have Vector3D.
                    if (registerType == typeof(Point3D) && targetProjectType == TargetProjectType.WPF)
                        registerType = typeof(Vector3D);
                    break;
                case "point4d":
                    // Silverlight doesn't have Point4D.
                    if (registerType == typeof(Color) && targetProjectType == TargetProjectType.WPF)
                        registerType = typeof(Point4D);
                    break;
            }

            if ((registerType == typeof(int) || registerType == typeof(uint)) && targetProjectType == TargetProjectType.WPF)
            {
                switch (suggestedType)
                {
                    case "byte":
                        registerType = typeof(byte);
                        break;
                    case "sbyte":
                        registerType = typeof(sbyte);
                        break;
                    case "short":
                        registerType = typeof(short);
                        break;
                    case "ushort":
                        registerType = typeof(ushort);
                        break;
                    case "char":
                        registerType = typeof(char);
                        break;
                    case "long":
                        registerType = typeof(long);
                        break;
                    case "ulong":
                        registerType = typeof(ulong);
                        break;
                }
            }
        }

        /// <summary>
        /// Returns a valid C# or Visual Basic identifier based on the given string.
        /// </summary>
        private static string GetValidId(string firstChoice, string secondChoice, bool isDotAllowed)
        {
            if (String.IsNullOrEmpty(firstChoice))
            {
                firstChoice = secondChoice;
            }

            StringBuilder stringBuilder = new StringBuilder();
            foreach (char c in firstChoice)
            {
                if (c == '_' || Char.IsLetter(c) || (stringBuilder.Length > 0 && (Char.IsDigit(c) || (c == '.' && isDotAllowed))))
                {
                    stringBuilder.Append(c);
                }
                else
                {
                    stringBuilder.Append('_');
                }
            }
            return stringBuilder.ToString();
        }

        /// <summary>
        /// Sets the out parameters to the standard min, max, and default values for the given type.
        /// </summary>
        private static void GetStandardValues(Type registerType, out object defaultValue)
        {
            if (registerType == typeof(double))
            {
                defaultValue = 0.0;
            }
            else if (registerType == typeof(float))
            {
                defaultValue = 0f;
            }
            else if (registerType == typeof(Point))
            {
                defaultValue = new Point(0, 0);
            }
            else if (registerType == typeof(Size))
            {
                defaultValue = new Size(0, 0);
            }
            else if (registerType == typeof(Vector))
            {
                defaultValue = new Vector(0, 0);
            }
            else if (registerType == typeof(Point3D))
            {
                defaultValue = new Point3D(0, 0, 0);
            }
            else if (registerType == typeof(Vector3D))
            {
                defaultValue = new Vector3D(0, 0, 0);
            }
            else if (registerType == typeof(Point4D))
            {
                defaultValue = new Point4D(0, 0, 0, 0);
            }
            else if (registerType == typeof(Color))
            {
                defaultValue = Colors.Black;
            }
            else if (registerType == typeof(bool))
            {
                defaultValue = false;
            }
            else if (registerType == typeof(int))
            {
                defaultValue = (int)0;
            }
            else if (registerType == typeof(uint))
            {
                defaultValue = (uint)0;
            }
            else if (registerType == typeof(byte))
            {
                defaultValue = (byte)0;
            }
            else if (registerType == typeof(sbyte))
            {
                defaultValue = (sbyte)0;
            }
            else if (registerType == typeof(short))
            {
                defaultValue = (short)0;
            }
            else if (registerType == typeof(ushort))
            {
                defaultValue = (ushort)0;
            }
            else if (registerType == typeof(char))
            {
                defaultValue = (char)0;
            }
            else if (registerType == typeof(long))
            {
                defaultValue = (long)0;
            }
            else if (registerType == typeof(ulong))
            {
                defaultValue = (ulong)0;
            }
            else
            {
                defaultValue = null;
            }
        }

        /// <summary>
        /// Converts the given string value into a double, Point, Point3D, or Color.
        /// </summary>
        private static void ConvertValue(string valueText, Type type, ref object value)
        {
            try
            {
                if (type == typeof(double))
                {
                    value = Double.Parse(valueText);
                }
                else if (type == typeof(float))
                {
                    value = Single.Parse(valueText);
                }
                else if (type == typeof(Point))
                {
                    value = Point.Parse(valueText);
                }
                else if (type == typeof(Size))
                {
                    value = Size.Parse(valueText);
                }
                else if (type == typeof(Vector))
                {
                    value = Vector.Parse(valueText);
                }
                else if (type == typeof(Point3D))
                {
                    value = Point3D.Parse(valueText);
                }
                else if (type == typeof(Vector3D))
                {
                    value = Vector3D.Parse(valueText);
                }
                else if (type == typeof(Point4D))
                {
                    value = Point4D.Parse(valueText);
                }
                else if (type == typeof(Color))
                {
                    Point4D point = Point4D.Parse(valueText);
                    value = Color.FromArgb((byte)point.X, (byte)point.Y, (byte)point.Z, (byte)point.W);
                }
                else if (type == typeof(bool))
                {
                    value = Boolean.Parse(valueText);
                }
                else if (type == typeof(int))
                {
                    value = int.Parse(valueText);
                }
                else if (type == typeof(uint))
                {
                    value = uint.Parse(valueText);
                }
                else if (type == typeof(byte))
                {
                    value = byte.Parse(valueText);
                }
                else if (type == typeof(sbyte))
                {
                    value = sbyte.Parse(valueText);
                }
                else if (type == typeof(short))
                {
                    value = short.Parse(valueText);
                }
                else if (type == typeof(ushort))
                {
                    value = ushort.Parse(valueText);
                }
                else if (type == typeof(char))
                {
                    value = char.Parse(valueText);
                }
                else if (type == typeof(long))
                {
                    value = long.Parse(valueText);
                }
                else if (type == typeof(ulong))
                {
                    value = ulong.Parse(valueText);
                }
            }
            catch
            {
                //TODO Report errors to UI
            }
        }

        /// <summary>
        /// Parses the string representation of an HLSL float, float2, float3, or float4 value,
        /// setting the final parameter to the corresponding double, Point, Point3D, or Color if possible.
        /// </summary>
        private static void ParseInitializerValue(string initializerValueText, Type registerType, ref object initializerValue)
        {
            if (registerType == typeof(bool))
            {
                bool outBool;
                if (Boolean.TryParse(initializerValueText, out outBool))
                {
                    initializerValue = outBool;
                }
                return;
            }

            double[] numbers = ParseNumbers(initializerValueText);
            if (registerType == typeof(double) && numbers.Length >= 1)
            {
                initializerValue = numbers[0];
            }
            else if (registerType == typeof(float) && numbers.Length >= 1)
            {
                initializerValue = (float)numbers[0];
            }
            else if (registerType == typeof(Point) && numbers.Length >= 2)
            {
                initializerValue = new Point(numbers[0], numbers[1]);
            }
            else if (registerType == typeof(Size) && numbers.Length >= 2)
            {
                initializerValue = new Size(Math.Max(0, numbers[0]), Math.Max(0, numbers[1]));
            }
            else if (registerType == typeof(Vector) && numbers.Length >= 2)
            {
                initializerValue = new Vector(numbers[0], numbers[1]);
            }
            else if (registerType == typeof(Point3D) && numbers.Length >= 3)
            {
                initializerValue = new Point3D(numbers[0], numbers[1], numbers[2]);
            }
            else if (registerType == typeof(Vector3D) && numbers.Length >= 3)
            {
                initializerValue = new Vector3D(numbers[0], numbers[1], numbers[2]);
            }
            else if (registerType == typeof(Point4D) && numbers.Length >= 4)
            {
                initializerValue = new Point4D(numbers[0], numbers[1], numbers[2], numbers[3]);
            }
            else if (registerType == typeof(Color) && numbers.Length >= 4)
            {
                initializerValue = Color.FromArgb(ConvertToByte(numbers[3]), ConvertToByte(numbers[0]), ConvertToByte(numbers[1]), ConvertToByte(numbers[2]));
            }
            else if ((registerType == typeof(int) || registerType == typeof(uint)) && numbers.Length >= 1)
            {
                initializerValue = numbers[0];
            }
        }

        /// <summary>
        /// Parses the string representation of an HLSL float, float2, float3, or float4 value,
        /// returning an array of doubles (possibly empty).
        /// </summary>
        private static double[] ParseNumbers(string text)
        {
            // Get rid of any leading "float(", "float2(", "float3(", or "float4" and trailing ")".
            text = Regex.Replace(text, @"^\s*float[1234]?\s*\((.*)\)\s*$", @"$1");

            // Split at commas.
            string[] textValues = text.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

            // Parse the numbers.
            List<double> numbers = new List<double>();
            foreach (string textValue in textValues)
            {
                string trimmedValue = textValue.Trim();
                double number;
                if (Double.TryParse(trimmedValue, out number))
                {
                    numbers.Add(number);
                }
                else
                {
                    break;
                }
            }
            return numbers.ToArray();
        }

        /// <summary>
        /// Converts a double-precision floating point number between 0 and 1 to a byte.
        /// </summary>
        private static byte ConvertToByte(double number)
        {
            return (byte)Math.Max(0, Math.Min(Math.Round(255 * number), 255));
        }
    }
}
